#include "core/command-line.hpp"

/**
 * \file
 * \ingroup commandline
 * cas::CommandLine implementation.
 */

namespace cas
{

    SLOG_COMPONENT_DEFINE("CommandLine");

    CommandLine::CommandLine()
        : m_NNonOptions(0),
          m_nonOptionCount(0),
          m_usage()
    {
        SLOG_FUNCTION(this);
    }

    CommandLine::CommandLine(const CommandLine &cmd)
    {
        Copy(cmd);
    }
    CommandLine &
    CommandLine::operator=(const CommandLine &cmd)
    {
        Clear();
        Copy(cmd);
        return *this;
    }
    CommandLine::~CommandLine()
    {
        SLOG_FUNCTION(this);
        Clear();
    }
    void
    CommandLine::Copy(const CommandLine &cmd)
    {
        SLOG_FUNCTION(&cmd);
        std::copy(cmd.m_options.begin(), cmd.m_options.end(), m_options.end());
        std::copy(cmd.m_nonOptions.begin(), cmd.m_nonOptions.end(), m_nonOptions.end());

        m_NNonOptions = cmd.m_NNonOptions;
        m_nonOptionCount = 0;
        m_usage = cmd.m_usage;
    }
    void
    CommandLine::Clear(void)
    {
        SLOG_FUNCTION(this);
        for (auto i : m_options)
        {
            delete i;
        }
        for (auto i : m_nonOptions)
        {
            delete i;
        }
        m_options.clear();
        m_nonOptions.clear();
        m_NNonOptions = 0;
        m_usage = "";
    }

    void
    CommandLine::Usage(const std::string usage)
    {
        m_usage = usage;
    }

    CommandLine::Item::~Item()
    {
        SLOG_FUNCTION(this);
    }

    void
    CommandLine::Parse(std::vector<std::string> args)
    {
        SLOG_FUNCTION(this << args.size() << args);
        m_nonOptionCount = 0;
        if (args.size() > 0)
        {
            args.erase(args.begin()); // discard the program name
            for (auto param : args)
            {
                if (HandleOption(param))
                {
                    continue;
                }
                if (HandleNonOption(param))
                {
                    continue;
                }

                // is this possible?
                SASSERT_MSG(false, "unexpected error parsing command line parameter: '" << param << "'");
            }
        }
    }

    bool
    CommandLine::HandleOption(const std::string &param) const
    {
        // remove leading "--" or "-"
        std::string arg = param;
        std::string::size_type cur = arg.find("--");
        if (cur == 0)
        {
            arg = arg.substr(2, arg.size() - 2);
        }
        else
        {
            cur = arg.find("-");
            if (cur == 0)
            {
                arg = arg.substr(1, arg.size() - 1);
            }
            else
            {
                // non-option argument?
                return false;
            }
        }
        // find any value following '='
        cur = arg.find("=");
        std::string name, value;
        if (cur == std::string::npos)
        {
            name = arg;
            value = "";
        }
        else
        {
            name = arg.substr(0, cur);
            value = arg.substr(cur + 1, arg.size() - (cur + 1));
        }
        HandleArgument(name, value);

        return true;
    }

    bool
    CommandLine::HandleNonOption(const std::string &value)
    {
        SLOG_FUNCTION(this << value);

        if (m_nonOptionCount == m_nonOptions.size())
        {
            // Add an unspecified non-option as a string
            SLOG_LOGIC("adding StringItem, NOCount:" << m_nonOptionCount
                                                     << ", NOSize:" << m_nonOptions.size());
            StringItem *item = new StringItem;
            item->m_name = "extra-non-option-argument";
            item->m_help = "Extra non-option argument encountered.";
            item->m_value = value;
            m_nonOptions.push_back(item);
        }

        auto i = m_nonOptions[m_nonOptionCount];
        if (!i->Parse(value))
        {
            std::cerr << "Invalid non-option argument value "
                      << value << " for " << i->m_name
                      << std::endl;
            PrintHelp(std::cerr);
            std::exit(1);
        }
        ++m_nonOptionCount;
        return true;
    }

    void
    CommandLine::Parse(int argc, char *argv[])
    {
        SLOG_FUNCTION(this << argc);
        std::vector<std::string> args(argv, argv + argc);
        Parse(args);
    }

    void
    CommandLine::PrintHelp(std::ostream &os) const
    {
        SLOG_FUNCTION(this);

        // Hack to show just the declared non-options
        Items nonOptions(m_nonOptions.begin(),
                         m_nonOptions.begin() + m_NNonOptions);
        os << (m_options.size() ? " [Program Options]" : "")
           << (nonOptions.size() ? " [Program Arguments]" : "")
           << " [General Arguments]"
           << std::endl;

        if (m_usage.length())
        {
            os << std::endl;
            os << m_usage << std::endl;
        }

        std::size_t width = 0;
        for (auto it : m_options)
        {
            width = std::max(width, it->m_name.size());
        }
        for (auto it : nonOptions)
        {
            width = std::max(width, it->m_name.size());
        }
        width += 3; // room for ":  " between option and help

        if (!m_options.empty())
        {
            os << std::endl;
            os << "Program Options:" << std::endl;
            for (auto i : m_options)
            {
                os << "    --"
                   << std::left << std::setw(width) << (i->m_name + ":")
                   << std::right
                   << i->m_help;

                if (i->HasDefault())
                {
                    os << " [" << i->GetDefault() << "]";
                }
                os << std::endl;
            }
        }

        if (!nonOptions.empty())
        {
            width += 2; // account for "--" added above
            os << std::endl;
            os << "Program Arguments:" << std::endl;
            for (auto i : nonOptions)
            {
                os << "    "
                   << std::left << std::setw(width) << (i->m_name + ":")
                   << std::right
                   << i->m_help;

                if (i->HasDefault())
                {
                    os << " [" << i->GetDefault() << "]";
                }
                os << std::endl;
            }
        }

        os << std::endl;
        os
            << "General Arguments:\n"
            << "    --PrintGlobals:              Print the list of globals.\n"
            << "    --PrintVersion:              Print the cas version.\n"
            << "    --PrintHelp:                 Print this help message.\n"
            << std::endl;
    }

    std::string
    CommandLine::GetVersion() const
    {
        return "0.1.0";
    }

    void
    CommandLine::PrintVersion(std::ostream &os) const
    {
        os << GetVersion() << std::endl;
    }

    void
    CommandLine::PrintGlobals(std::ostream &os) const
    {
        SLOG_FUNCTION(this);

        os << "Global values:" << std::endl;

        // Sort output
        std::vector<std::string> globals;

        for (GlobalValue::Iterator i = GlobalValue::Begin();
             i != GlobalValue::End();
             ++i)
        {
            std::stringstream ss;
            ss << "    --" << (*i)->GetName() << "=["
               << (*i)->GetValue() << "]" << std::endl
               << "        " << (*i)->GetHelp() << std::endl;
            globals.push_back(ss.str());
        }
        std::sort(globals.begin(), globals.end());
        for (std::vector<std::string>::const_iterator it = globals.begin();
             it < globals.end();
             ++it)
        {
            os << *it;
        }
    }

    void
    CommandLine::HandleArgument(const std::string &name, const std::string &value) const
    {
        SLOG_FUNCTION(this << name << value);

        SLOG_DEBUG("Handle arg name=" << name << " value=" << value);

        // Hard-coded options
        if (name == "PrintHelp" || name == "help")
        {
            // method below never returns.
            PrintHelp(std::cout);
            std::exit(0);
        }
        if (name == "PrintVersion" || name == "version")
        {
            // Print the version, then exit the program
            PrintVersion(std::cout);
            std::exit(0);
        }
        else if (name == "PrintGlobals")
        {
            // method below never returns.
            PrintGlobals(std::cout);
            std::exit(0);
        }
        else
        {
            for (const auto &i : m_options)
            {
                if (i->m_name == name)
                {
                    if (!i->Parse(value))
                    {
                        std::cerr << "Invalid argument value: "
                                  << name << "=" << value << std::endl;
                        PrintHelp(std::cerr);
                        std::exit(1);
                    }
                    else
                    {
                        return;
                    }
                }
            }
        }
        // Fallback
        if (!GlobalValue::BindFailSafe(name, value))
        {
            std::cerr << "Invalid command-line arguments: --"
                      << name << "=" << value << std::endl;
            PrintHelp(std::cerr);
            std::exit(1);
        }
    }

    std::string
    CommandLine::GetExtraNonOption(std::size_t i) const
    {
        std::string value;

        if (m_nonOptions.size() >= i + m_NNonOptions)
        {
            auto ip = dynamic_cast<StringItem *>(m_nonOptions[i + m_NNonOptions]);
            if (ip != NULL)
            {
                value = ip->m_value;
            }
        }
        return value;
    }

    std::size_t
    CommandLine::GetNExtraNonOptions(void) const
    {
        if (m_nonOptions.size() > m_NNonOptions)
        {
            return m_nonOptions.size() - m_NNonOptions;
        }
        else
        {
            return 0;
        }
    }

    /* static */
    bool
    CommandLine::Item::HasDefault() const
    {
        return false;
    }

    bool
    CommandLine::StringItem::Parse(const std::string value)
    {
        m_value = value;
        return true;
    }

    bool
    CommandLine::StringItem::HasDefault(void) const
    {
        return false;
    }

    std::string
    CommandLine::StringItem::GetDefault(void) const
    {
        return "";
    }

    template <>
    std::string
    CommandLineHelper::GetDefault<bool>(const bool &val)
    {
        std::ostringstream oss;
        oss << std::boolalpha << val;
        return oss.str();
    }

    template <>
    bool
    CommandLineHelper::UserItemParse<bool>(const std::string value, bool &val)
    {
        std::string src = value;
        std::transform(src.begin(), src.end(), src.begin(),
                       [](char c)
                       { return static_cast<char>(std::tolower(c)); });
        if (src.length() == 0)
        {
            val = !val;
            return true;
        }
        else if ((src == "true") || (src == "t"))
        {
            val = true;
            return true;
        }
        else if ((src == "false") || (src == "f"))
        {
            val = false;
            return true;
        }
        else
        {
            std::istringstream iss;
            iss.str(src);
            iss >> val;
            return !iss.bad() && !iss.fail();
        }
    }

    template <>
    bool
    CommandLineHelper::UserItemParse<uint8_t>(const std::string value, uint8_t &val)
    {
        uint8_t oldVal = val;
        long newVal;

        try
        {
            newVal = std::stoi(value);
        }
        catch (std::invalid_argument &ia)
        {
            SLOG_WARN("invalid argument: " << ia.what());
            val = oldVal;
            return false;
        }
        catch (std::out_of_range &oor)
        {
            SLOG_WARN("out of range: " << oor.what());
            val = oldVal;
            return false;
        }
        if (newVal < 0 || newVal > 255)
        {
            return false;
        }
        val = newVal;
        return true;
    }

    std::ostream &
    operator<<(std::ostream &os, const CommandLine &cmd)
    {
        cmd.PrintHelp(os);
        return os;
    }

} // namespace ns3
